Repository navigation
fix(host): stop storing the host's seq as a guest room's joined floor - #2613
Merged
Merged
Conversation
applyInboundRoomShare copied the wire `seq` - the host room's own line counter - into the guest's `sharedFrom.joinedSeq`, while the guest numbers its mirrored log from 1. The history the join carried, and every line after it, were filtered out until the host's counter caught up, and a malformed seq left the room permanently empty. Refs #2612
Collaborator
Author
|
Verdict: PASS at db5ea17 Reviewed tip Scope and conformance
Ran:
Not run:
|
HugoXK
added a commit
to HugoXK/FlyBot
that referenced
this pull request
Nov 10, 2026
…ws nothing, and never heals (ThinkFlowLab#2759) Fixes ThinkFlowLab#2754 Owner: Dev-1 Written-by: Dev-1 Bot: Dev-1 Claim-Id: 020fcfd92ed647d0b83d1fce58a70ae6 Base: f2ef4df Risk tier: T3 ## What changed A room whose stored join floor sits above its own `room.seq` shows no lines at all, and every new line lands below the floor too, so the room never heals: `joinedSeqOf` returned the stored value unchanged and both read entries filtered by it. The floor comes from a host counter that reached a participant record (or `sharedFrom`) before ThinkFlowLab#2612/ThinkFlowLab#2613 stopped writing it. After: the computed floor is clamped on the read path. When it is above the room's own `room.seq`, both `rt.linesVisibleTo` and `rt.roomHistory` read from `room.seq` instead, so a room at `room.seq = 2` with a stored floor of 202 shows the line at seq 2 (and every line that arrives later), while a healthy floor is untouched. The clamped value is memoised per room and reader at the `room.seq` of the first read — recomputing it against the seq of every later read would slide the window forward and hide the line the guest joined at — and that memo is read before the "floor is healthy" early return: without that, the room goes dark a second time from the other side, because once `room.seq` reaches the stored floor the lookup stops clamping and answers with the stored floor again, dropping every row the reader had already seen. A reader that really rejoins (a new `joinedSeq`, or the same number joined again later, a new `joinedAt`) gets a new interval, so its memo entry is dropped rather than reused. One `warn` per room names the room id, the pre-clamp floor and `room.seq`. The state object is never rewritten, so no later `save()` can turn the clamp into a fact on disk. The module-level `joinedSeqOf` / `linesVisibleTo` stay pure; only the instance-level adapters pass through the clamp. A room saved before `seq` existed has no counter to clamp against. Its log's last seq is the number `ensureLog` would write on the next append, so that is used instead of 0 — clamping to 0 would show the guest the lines from before it joined, the over-read this fix must not create. Issue shape note: as the issue records after its 2026-11-10T00:18:20Z update, A (participants 202 / sharedFrom 202) and A2 (participants 202 / sharedFrom 0) assert **1 row**; A3 (participants 0 / sharedFrom 202) asserts **2 rows** because the participant's value wins, the clamp does not fire, and that is the positive control that the priority rule was not inverted. After mirroring a line at seq 3, A and A2 assert **2 rows**. ## Verification at the tip Tip SHA: 8eead21 Tree: 9690e6b — the same tree as the local verified commit `193036664dcea4df1961a29a9e74732d0e61731f`; `.flybot_push.py --update` re-committed that tree on the live base (`f2ef4dfacc`), so every run below was taken on this tree and this head names the PR tip. Ran: Second round (`8eead216`), the memo read before the early return: - red first on the previous tip `4033f570`, which carries the clamp but not this fix, before any source change in this round: `test/room-membership.test.js` `# tests 26 / # pass 25 / # fail 1` — `not ok 13 - ThinkFlowLab#2754: a clamped floor keeps its window once room.seq reaches the stored floor`, actual `['five']`, expected `['Bob joined', 'three', 'four', 'five']` at floor 5 / `room.seq` 5. The same test covers floor 202 with the room grown to `seq` 300 (299 rows, first row still `Bob joined`). - after the fix, the same file: `# tests 26 / # pass 26 / # fail 0`, including the rejoin case (new `joinedSeq` 6 at `joinedAt` 2 reads from `['six']`, not the old window). First round (this tip carries both changes), kept for the clamp itself: - red first on a clean `f2ef4dfaccedd40315bd585474d0902118dc0ba8` tree carrying only `core/test/room-membership.test.js` from this branch (`core/src/host/room-membership.js` at the base): `# tests 25 / # pass 23 / # fail 2` — `not ok 12 - ThinkFlowLab#2754: a stored floor above room.seq is clamped to room.seq on read` and `not ok 14 - ThinkFlowLab#2754: the clamp warns once per room and never rewrites the stored floor`. The pure-helper case (`0/1/2/3 -> 2/2/1/0`) passes at the base, as it must: the module-level filter is unchanged. - after the fix, the same file: `# tests 25 / # pass 25 / # fail 0` - `TZ=UTC npm run verify` with `ARCH_DOCS_BASE=f2ef4dfaccedd40315bd585474d0902118dc0ba8`, run on the committed tree `193036664d`: exit 0 — core `# tests 3140 / # pass 3140 / # fail 0` (the 26 room-membership cases are inside it), social 619, tree 140, timeline 161, workflow 508, `.github/scripts` + policy 671, interop 37, live-e2e 34 - `ARCH_DOCS_BASE=f2ef4dfaccedd40315bd585474d0902118dc0ba8 npm --prefix core run arch-docs`, re-run on the committed tree: `arch-docs: OK (7 changed file(s))` - `node scripts/ds-bot-patches.mjs generate` on this base wrote identical bytes; `check` before and after the commit: `10 patches apply cleanly in order`, `core/ equals ds-bot/ + patches/ds-bot/`; `canonical` after the commit: `ok patches/ds-bot/ is what generate writes (generate is a no-op)` - `node .github/scripts/changelog.mjs check`: `17 fragment(s) in changelog.d/ are well formed` Not run: `dev/bench.sh` — no bench was started for this branch; the change is a read-path filter in the host, covered by the room-membership suite. No real-model end-to-end run: the room read surface is host-local and the new cases drive it through the same `host()` fixture the file already uses. ## Deviation from a plain source edit `core/src/host/room-membership.js` and `core/test/room-membership.test.js` are carried by `patches/ds-bot/0012-room-membership.patch` through its `patches/ds-bot/files/` mirror (that patch stores those files as a mirror, not as patch-body hunks), so the branch carries both the source pair and the mirrored pair; `generate` on this base reports the series as what it writes. ## How an old reader sees it No API shape, field name, or stored value changes. A guest of a healthy room reads exactly the lines it read before, no `warn` fires for it, and the saved `joinedSeq` / `sharedFrom` are untouched. Only a room whose stored floor is above its own seq reads differently (from `room.seq` instead of nothing), and only that room warns once. Once such a room's own seq reaches the stored floor its window does not slide forward and disappear: it keeps reading from the seq it clamped to, until the reader rejoins. ## Checkpoint <!-- flybot:checkpoint --> ```json { "pr": 2759, "claim": "Dev-1", "issue": 2754, "summary": "A room whose stored join floor sits above its own room.seq rendered no lines and never healed, because every line and every new line fell below the floor. The read path clamps the computed floor to room.seq (to the log's last seq for a room saved before seq existed), for both rt.linesVisibleTo and rt.roomHistory, memoised per room and reader at the first read and warning once per room; the stored state is never rewritten, so no save turns the clamp into a fact on disk. The memo is read before the healthy-floor early return, so a room that grows past the stored floor keeps its window, and a rejoin drops it and reads from the new interval. Red first: 2 of 25 cases failed at f2ef4df, and 1 of 26 failed at 4033f570 for the growth case; the same file is 26/26 after the fix.", "files": [ "core/src/host/room-membership.js", "core/test/room-membership.test.js", "docs/subsystems/team.md", "docs/subsystems/core-host.md", "changelog.d/2754-room-floor-clamp.fixed.md", "patches/ds-bot/files/src/host/room-membership.js", "patches/ds-bot/files/test/room-membership.test.js" ], "checks": { "ran": "TZ=UTC npm run verify with ARCH_DOCS_BASE=f2ef4dfa; core arch-docs; node scripts/ds-bot-patches.mjs generate/check/canonical; node .github/scripts/changelog.mjs check", "result": "green", "at": "8eead216f75d92e51c0e8b01c9bb541de3ed9e0d" }, "next": "waiting for the review side's Verdict naming 8eead21" } ``` <!-- /flybot:checkpoint -->
HugoXK
added a commit
to HugoXK/FlyBot
that referenced
this pull request
Nov 10, 2026
… seq reads a full room as empty (ThinkFlowLab#2791) read_group_chat bypasses the join-floor clamp, so a Bot's floor above seq reads a full room as empty Fixes ThinkFlowLab#2790 Owner: Dev-1 Written-by: Dev-1 Bot: Dev-1 Reviewer: Birfy Risk tier: T3 Base: 74d08c5 Claim-Id: 4bc854af8bdd4889a31174abc5395f83 ## What changed `read_group_chat` resolved its floor from the Bot's own participant record and filtered `room.log` on the raw `joinedSeq`, so the ThinkFlowLab#2754 clamp — which the room reads of a human actor already run — never applied to a Bot's read. A stored floor above the room's own `room.seq` (the shape a host counter left on a participant record before ThinkFlowLab#2612/ThinkFlowLab#2613 stopped the write) therefore hid every line from that reader and answered `"<room>" has no messages yet` in a room the pane and every other reader still showed. The clamp body is now `clampedFloorOf(room, record, reader)` in `core/src/host/room-membership.js`, keeping the memo, the one-time warning and the no-`seq` fallback unchanged; the human reads resolve their record and call it exactly as before, and the Bot read asks for its own seat's floor through the new `rt.seatFloor(room, seat)`. The memo key now carries the seat kind, so a human seat and a Bot seat that happen to share an id cannot share a window. `core/src/host/groups.js` uses it and keeps `botPart?.joinedSeq` as the fallback for a host where the room-membership plugin is not installed. `docs/subsystems/core-host.md` records that a Bot's own read takes the same clamp, and one changelog fragment is added. ## Red first — clean base `74d08c5d`, only the new test on the branch ```text $ node --import ./test/harness-resolve.mjs --test test/room-read-floor-clamp.test.js not ok 1 - a Bot reads a room whose stored join floor is above room.seq a room with lines reads as empty: "Ops" has no messages yet. not ok 2 - the clamped window opens onto lines that arrive afterwards not ok 5 - a room without seq clamps to its stored log # tests 5 # pass 2 # fail 3 ``` The two guards that already pass are the healthy-floor read and an empty room's own sentence, so the red is only the defect's face. ## After the fix ```text $ node --import ./test/harness-resolve.mjs --test test/room-read-floor-clamp.test.js # tests 5 # pass 5 # fail 0 $ node --import ./test/harness-resolve.mjs --test test/room-*.test.js test/group*.test.js test/face-*.test.js test/details-group-*.test.js test/mock-group-roster.test.js test/link-room-title.test.js # 33 files: # tests 254 # pass 254 # fail 0 $ cd plugins/workflow && node --import ./test/resolve.mjs --test "test/*.test.js" # tests 518 # pass 518 # fail 0 ``` ## Generated mirrors `node scripts/ds-bot-patches.mjs generate` wrote `patches/ds-bot/files/ (391 files)` and all ten patch files. Against this base the change is exactly three generated paths: - `patches/ds-bot/0012-room-membership.patch` +6/−2 (the `read_group_chat` hunk) - `patches/ds-bot/files/src/host/room-membership.js` +10/−3 - `patches/ds-bot/files/test/room-read-floor-clamp.test.js` (new, 111 lines) `check` answers all three lines ok — `ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)`, `10 patches apply cleanly in order`, `core/ equals ds-bot/ + patches/ds-bot/` — and `canonical` reports `patches/ds-bot/ is what generate writes (generate is a no-op)`. ## Ran - `TZ=UTC npm run verify` — exit 0: core `# tests 3190 / # pass 3190`, social 619, tree 143, timeline 161, workflow 518, `.github/scripts` + policy 671, interop 37, live-e2e 34, zero failures. - `node scripts/lint.mjs` — `Found 88 warnings and 0 errors` (the warnings are on `main` too). - `ARCH_DOCS_BASE=74d08c5d npm --prefix core run arch-docs` — `arch-docs: OK (8 changed file(s))`. - `node .github/scripts/changelog.mjs check` — `20 fragment(s) in changelog.d/ are well formed`. - `node scripts/ds-bot-patches.mjs generate` / `check` / `canonical` as above. - Verified commit `685c3210` (tree `5060cdd3`), eight files `+247/−10`; the pushed head is generated from that tree by the push helper. ## Not run - the smoke test: no `index.js` and no `src/host/` module wiring change beyond the two functions above (the bench needs a harness checkout and a port). - `npm run pack`, typecheck and actionlint: not part of `npm run verify`. ## Checkpoint <!-- flybot:checkpoint --> ```json { "pr": 2791, "claim": "Dev-1", "issue": 2790, "summary": "read_group_chat filtered the room log on the Bot's raw joinedSeq, so the ThinkFlowLab#2754 clamp the human room reads already run never applied: a stored floor above room.seq hid every line and answered has no messages yet in a room others still read. The clamp body is now clampedFloorOf(room, record, reader) and a Bot's read asks rt.seatFloor for its own seat, so the memo, the one-time warning and the no-seq fallback stay one rule. A new core/test/room-read-floor-clamp.test.js covers the clamp, the window healing, healthy floors, an empty room and a room saved before seq. generate, check and canonical are green, and the core and plugin suites pass.", "files": ["core/src/host/groups.js", "core/src/host/room-membership.js", "core/test/room-read-floor-clamp.test.js", "docs/subsystems/core-host.md", "changelog.d/2790-read-group-floor-clamp.fixed.md", "patches/ds-bot/0012-room-membership.patch", "patches/ds-bot/files/src/host/room-membership.js", "patches/ds-bot/files/test/room-read-floor-clamp.test.js"], "checks": { "ran": "TZ=UTC npm run verify (exit 0); ds-bot-patches generate/check/canonical; ARCH_DOCS_BASE=74d08c5d npm --prefix core run arch-docs", "result": "green", "at": "a2b157e744106de819841b883a81f93592532a52" }, "next": "none — ready for review" } ``` <!-- /flybot:checkpoint -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Written-by: Dev
Fixes #2612
Owner: Dev
Reviewer: Reviewer
Why
A room created from an inbound
joinstored the envelope'sseq— the host room's own line counter — as the guest'ssharedFrom.joinedSeq. The guest numbers its mirrored log from 1 andlinesVisibleTokeeps a line only when(line.seq ?? 0) >= from. With a join carryingseq: 202and two history lines, the guest ended up withroom.seq = 2,joinedSeq = 202,log = [1, 2]:roomHistoryanswered 0 lines, and a new message (local seq 3) stayed invisible too, so the room stayed blank until the host's counter reached 202. A non-numericseq("abc",{},[1,2]) was stored as-is; the comparison against a string is always false, so that room never showed anything again.What changed
core/src/host/room-membership.js— the room-creation branch ofapplyInboundRoomShareno longer writesjoinedSeq: data.seq, so the existingdraft.joinedSeq ?? 0applies, exactly the way the re-seat path already behaves. The wireseqis the host's counter and is not a floor on this side. No clamp was added tolinesVisibleTo: the filter is shared by every reader, and hiding the value there would keep the wrong state instead of fixing it.core/test/room-membership.test.js— three#2612cases in the existing file: aseq: 202join carrying only the last two history lines shows both of them to the invited Bot; a line that arrives after the join is visible every time; and junkseqvalues ("abc",{},[1,2], sent as raw envelope text becauseencodeRoomSharecoercesseq) never land instate.jsonand never empty the room. All three fail on the unchanged source.docs/subsystems/core-host.md— one invariant sentence in the room-share bullet: a mirror's membership floor is the guest's own log, never the envelope. (core-host.mdownscore/src/host/.)changelog.d/2612.fixed.md— the user-visible line.patches/ds-bot/files/...— the two regenerated mirrors, byte-identical tocore/(sha256 compared;checkthen reportscore/ equals ds-bot/ + patches/ds-bot/).Ran at this tip:
Tip
db5ea17b90d3c5d862db2b4cb3037bab55f3f8ac, built on the livemain836b83c — that commit plus these six files, byte for byte. The red run above is the same tip with onlyroom-membership.js's removed line restored.Not run, and why
npm run verifyat the repo root — this box is shared with other lines and the task scopes verification to the target tests; two full suites would exhaust it.node scripts/build-client.mjs --check—core/node_modulesis not installed here (esbuildmissing); nocore/src/client/file changed, soclient.jsis untouched.core/scripts/check-arch-docs.mjs— needsgitto diff againstorigin/main, unavailable here; the owned page is updated in this change, so the rule is satisfied and CI runs it.Notes
state.jsonholdsjoinedSeq: 202) stays as it is until it is re-seated; there is no migration in this change, per Triage's out-of-scope line.seqon the wire for older guests; only the guest stops storing it as a floor.Checkpoint
{ "pr": 2613, "claim": "Dev", "issue": 2612, "summary": "A guest room created from a share no longer copies the envelope's seq into sharedFrom.joinedSeq, so the history the join carried and every later line are visible instead of being filtered out until the host's line counter caught up. A malformed seq can no longer become a stored floor that empties the room. Three tests cover a seq-202 join, a line after the join, and junk seq values; all three fail without the change. The subsystem page records that a mirror's membership floor is the guest's own log, never the envelope.", "files": [ "core/src/host/room-membership.js", "core/test/room-membership.test.js", "docs/subsystems/core-host.md", "changelog.d/2612.fixed.md", "patches/ds-bot/files/src/host/room-membership.js", "patches/ds-bot/files/test/room-membership.test.js" ], "checks": { "ran": "cd core && node --import ./test/harness-resolve.mjs --test test/room-membership.test.js; node scripts/ds-bot-patches.mjs check; oxlint; changelog.mjs check; check-mojibake.mjs", "result": "green", "at": "db5ea17b90d3c5d862db2b4cb3037bab55f3f8ac" }, "next": "none — ready for review" }